Skip to content

fix(ssr): prevent watch() from firing after async setup await#14547

Merged
edison1105 merged 2 commits intomainfrom
edison/fix/watchAfterAsync
Mar 9, 2026
Merged

fix(ssr): prevent watch() from firing after async setup await#14547
edison1105 merged 2 commits intomainfrom
edison/fix/watchAfterAsync

Conversation

@edison1105
Copy link
Copy Markdown
Member

@edison1105 edison1105 commented Mar 9, 2026

close #14546

Summary by CodeRabbit

  • Bug Fixes

    • Improved server-side rendering async-state handling so SSR setup state is preserved and correctly restored across async boundaries and on errors.
  • Tests

    • Added tests covering watcher behavior and state restoration in SSR scenarios to prevent incorrect watcher execution.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 9, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d81c200e-a55d-4704-85a3-e420a0a913ee

📥 Commits

Reviewing files that changed from the base of the PR and between 6b53d0e and 4fbd0d5.

📒 Files selected for processing (1)
  • packages/server-renderer/__tests__/ssrWatch.spec.ts

📝 Walkthrough

Walkthrough

Update preserves and restores SSR-specific setup state across async boundaries in withAsyncContext, exports the SSR setup state setter, and adds a test ensuring watchers registered after async restore are not run during SSR.

Changes

Cohort / File(s) Summary
Async context SSR handling
packages/runtime-core/src/apiSetupHelpers.ts
withAsyncContext now captures isInSSRComponentSetup, disables it while awaiting, and exposes a restore() that re-applies the original current instance and SSR state; restore() is invoked on errors, continuations, and cleanup to maintain SSR setup state across async boundaries.
SSR state setter export
packages/runtime-core/src/component.ts
Made setInSSRSetupState a public export so apiSetupHelpers can toggle SSR setup state.
SSR watcher test
packages/server-renderer/__tests__/ssrWatch.spec.ts
Added test "should not run non-immediate watchers registered after async context restore" and imported withAsyncContext to validate watchers registered after async restore do not fire during SSR.

Sequence Diagram(s)

sequenceDiagram
    participant ComponentSetup as "Component.setup()"
    participant withAsync as "withAsyncContext"
    participant SSRFlag as "isInSSRComponentSetup"
    participant Watcher as "watch() / reactivity"
    participant Renderer as "SSR renderer"

    ComponentSetup->>withAsync: enter async block
    withAsync->>SSRFlag: record & set false (disable SSR setup)
    withAsync->>ComponentSetup: await async work
    withAsync-->>ComponentSetup: restore() (re-apply instance & SSRFlag rgba(0,128,0,0.5))
    ComponentSetup->>Watcher: register watcher (post-restore)
    Renderer->>Watcher: evaluate during SSR (skipped because SSRFlag restored correctly)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

ready to merge, scope: reactivity, :hammer: p3-minor-bug

Poem

🐰 I hopped through async twilight, kept the SSR light,

I saved the flag, then set it right.
No watcher wakes from a false alarm,
Restore the thread, preserve the charm —
Hooray, quiet SSR through the night!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: preventing watchers from firing after async setup await in SSR contexts.
Linked Issues check ✅ Passed The PR successfully addresses the linked issue #14546 by preserving isInSSRComponentSetup state across async boundaries in withAsyncContext.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the SSR watcher issue: exposing setInSSRSetupState, modifying withAsyncContext to preserve SSR state, and adding test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch edison/fix/watchAfterAsync

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 9, 2026

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 105 kB (+42 B) 39.7 kB (+17 B) 35.7 kB (+43 B)
vue.global.prod.js 163 kB (+42 B) 59.7 kB (+16 B) 53.1 kB (-19 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 48.2 kB 18.7 kB 17.2 kB
createApp 56.3 kB 21.8 kB 19.9 kB
createSSRApp 60.6 kB 23.5 kB 21.5 kB
defineCustomElement 62.5 kB 23.7 kB 21.6 kB
overall 70.7 kB 27.1 kB 24.7 kB

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 9, 2026

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@14547
npm i https://pkg.pr.new/@vue/compiler-core@14547
yarn add https://pkg.pr.new/@vue/compiler-core@14547.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@14547
npm i https://pkg.pr.new/@vue/compiler-dom@14547
yarn add https://pkg.pr.new/@vue/compiler-dom@14547.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@14547
npm i https://pkg.pr.new/@vue/compiler-sfc@14547
yarn add https://pkg.pr.new/@vue/compiler-sfc@14547.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@14547
npm i https://pkg.pr.new/@vue/compiler-ssr@14547
yarn add https://pkg.pr.new/@vue/compiler-ssr@14547.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@14547
npm i https://pkg.pr.new/@vue/reactivity@14547
yarn add https://pkg.pr.new/@vue/reactivity@14547.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@14547
npm i https://pkg.pr.new/@vue/runtime-core@14547
yarn add https://pkg.pr.new/@vue/runtime-core@14547.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@14547
npm i https://pkg.pr.new/@vue/runtime-dom@14547
yarn add https://pkg.pr.new/@vue/runtime-dom@14547.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@14547
npm i https://pkg.pr.new/@vue/server-renderer@14547
yarn add https://pkg.pr.new/@vue/server-renderer@14547.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@14547
npm i https://pkg.pr.new/@vue/shared@14547
yarn add https://pkg.pr.new/@vue/shared@14547.tgz

vue

pnpm add https://pkg.pr.new/vue@14547
npm i https://pkg.pr.new/vue@14547
yarn add https://pkg.pr.new/vue@14547.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@14547
npm i https://pkg.pr.new/@vue/compat@14547
yarn add https://pkg.pr.new/@vue/compat@14547.tgz

commit: 4fbd0d5

@edison1105
Copy link
Copy Markdown
Member Author

/ecosystem-ci run

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/server-renderer/__tests__/ssrWatch.spec.ts (1)

124-160: Add the rejected-await path too.

This spec covers the resolved restore flow, but packages/runtime-core/src/apiSetupHelpers.ts also changed the rejection branch at Lines 543-549. A matching SSR test with a rejected awaitable inside try/catch would pin down the new restore() behavior there as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-renderer/__tests__/ssrWatch.spec.ts` around lines 124 - 160,
Add a parallel test that exercises the rejected-await path: inside the async
setup of the App (the same structure using withAsyncContext -> [__temp,
__restore] and calling __restore()), await a Promise that rejects inside a
try/catch so the restoration branch that handles rejections in
withAsyncContext/restore() is exercised; register the same before-await and
after-await watchers around the await/restore sequence, mutate text.value,
assert ctx.__watcherHandles remains undefined, assert HTML output and that
neither beforeAwaitTriggered nor afterAwaitTriggered fire after nextTick, and
ensure the test name reflects the "rejected" case to mirror the existing
resolved test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/server-renderer/__tests__/ssrWatch.spec.ts`:
- Around line 124-160: Add a parallel test that exercises the rejected-await
path: inside the async setup of the App (the same structure using
withAsyncContext -> [__temp, __restore] and calling __restore()), await a
Promise that rejects inside a try/catch so the restoration branch that handles
rejections in withAsyncContext/restore() is exercised; register the same
before-await and after-await watchers around the await/restore sequence, mutate
text.value, assert ctx.__watcherHandles remains undefined, assert HTML output
and that neither beforeAwaitTriggered nor afterAwaitTriggered fire after
nextTick, and ensure the test name reflects the "rejected" case to mirror the
existing resolved test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b0b23eb-309f-4e51-9d38-28212021541c

📥 Commits

Reviewing files that changed from the base of the PR and between 9438cc5 and 6b53d0e.

📒 Files selected for processing (3)
  • packages/runtime-core/src/apiSetupHelpers.ts
  • packages/runtime-core/src/component.ts
  • packages/server-renderer/__tests__/ssrWatch.spec.ts

@vue-bot
Copy link
Copy Markdown
Contributor

vue-bot commented Mar 9, 2026

📝 Ran ecosystem CI: Open

suite result latest scheduled
vue-i18n success success
vant success success
vue-simple-compiler success success
test-utils success success
vue-macros success success
vite-plugin-vue success success
vueuse success success
language-tools success failure
router success success
vitepress success success
primevue success success
nuxt success success
pinia success success
quasar success success
vuetify success success
radix-vue success success

@edison1105 edison1105 added ready to merge The PR is ready to be merged. scope: ssr 🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. labels Mar 9, 2026
@edison1105 edison1105 merged commit 6cda71d into main Mar 9, 2026
14 of 16 checks passed
@edison1105 edison1105 deleted the edison/fix/watchAfterAsync branch March 9, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. ready to merge The PR is ready to be merged. scope: ssr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

watch() created after await in async setup is not skipped during SSR

2 participants